
class SerialOSC {
  /*
  The SerialOSC class implements an OSC interface on a Serial Link
   
   The readString() method is called to process the String Buffer at each serialEvent(Serial p) Callback
   The following bit of code should exist at the global level to handle the serialEvent callback:
   
   void serialEvent(Serial p) { 
     SerialOSC.readString(); 
   }
   
   Note - if this class is sub-classed, readString() or similar serialEvent callback must leave an Ack 
   message on the stack for the .connect() method to confirm connection with the remote end.
   
   The character that initiates the Callback via the Serial.bufferUntil() call is managed by this class
   
   */

  // ******************************************************
  // Properties
  Serial thePort;
  PApplet pA;
  ArrayList msgStack;                            // An Array List of SerialOSCmsg Objects (
  char start_char, num_char, term_char;
  String ports[];
  String port_list[] = {"Select Serial Port"};
  String port_name;
  boolean have_port, have_menu;
  int baudRate;
  SerialOSCmsg ackMsg = new SerialOSCmsg("K");  // Remote end must respond to an "ack" message
  String ackStr = "Kk";                         // With an OSC "ackStr" command
                                                // Note - these can be changed by setting the properties

  // ******************************************************
  // Constructor
  //SerialOSC() {
  SerialOSC(PApplet p) {
    // Constructor
    // Typical usage is SerialOSC osc = new SerialOSC(this);
    //
    // The main thread PApplet is required to provide a context
    // for the Serial Port object constructor, which is invoked 
    // by this class's .connect() method.
    pA = p;
    start_char = '/';
    term_char = 10;
    //str_char = '$';
    num_char = ',';
    have_port = false;
    have_menu = false;
    msgStack = new ArrayList();
  }
  
  // ******************************************************
  // begin(SerialPort) 
  // begin(SerialPort, term_char)
  // methods allow a SerialOSC object to be used by simply assigning
  // a Serial Port that has been instantiated directly by the main program
  public void begin(Serial p) {
    thePort = p;
    thePort.bufferUntil(term_char);
    have_port = true;
  }
  
  public void begin(Serial p, char term) {
    thePort = p;
    term_char = term;
    thePort.bufferUntil(term);
    have_port = true;
  }
  
  // ******************************************************
  // setTerm(term) Sets the bufferUntil character
  public void setTerm(char term) {
    term_char = term;
    if (have_port) thePort.bufferUntil(term);
  }
  
  // ******************************************************
  // doMenu(GDropList, GLabel)
  // Builds a Serial Port Menu and Provides Status Information
  // in the G4P Widgets provided
  public void doMenu(GDropList Menu, GLabel statusLabel) {
    ports = Serial.list();
    have_menu = true;
    boolean need_name = true;
    int selected = 0;
    if (ports.length > 0) {
      for(int i=0; i<ports.length; i++) {
        // on mac & linux we'll get rid of the /dev/ and just show the name
        String parts[] = split(ports[i],'/');
        String the_port = parts[parts.length-1];
        port_list = append(port_list, the_port);
        String[] temp_str_2 = match(port_name, the_port);
        if (temp_str_2 != null) { 
          selected = i + 1;
          need_name = false;
        }
      }
    }
    Menu.setItems(port_list, selected);
    if (need_name) statusLabel.setText("Could not find port:\n" + port_name);
  }
  
  // ******************************************************
  // connect(GDropList, GButton, GLabel, bRate, term)
  // Opens a Serial Port specified by the G4P Menu Widget (set up by doMenu())
  // Port is opened at Baud Rate bRate, and Buffers until character term
  // Provides Status information via the G4P Button and Label Widgets
  public void connect(GDropList menu, GButton connectButton, GLabel connectStatus, int bRate, char term) {
    baudRate = bRate;
    term_char = term;
    if (!have_port) {
      // Get the index of the selected serial port
      if (have_menu) {
        int idx = menu.getSelectedIndex(); 
        // "0" = "Select Serial Port" - so only if we have a valid selection
        if (idx > 0) {
          String selectPort = ports[idx - 1];
          //println("Opening Serial Port");
          displayText.scrollText("Opening Serial Port");
          thePort = new Serial(pA, selectPort, bRate);
          //println("Instantiated Serial Port");
          have_port = true;
          thePort.clear();  
          thePort.bufferUntil(term);
          send(ackMsg);
          int start_time = millis();
          int now = start_time;
          int timeout = start_time + 5000;
          boolean waiting = true;
         
         // Need to see if we get an "Ack" back
         //println("Waiting for Ack");
          while (waiting) {
            now = millis();
            if (available()) {
              //println("About to read OSC Message");
              SerialOSCmsg msg = read();
              //println("Got Message");
              waiting = false;
              have_port = true;
              connectStatus.setText("Connected");
              port_name = selectPort;
              connectButton.setText("Disconnect");
              menu.setEnabled(false);
            }
            if (now > start_time + 500) {
              start_time = now;
              send(ackMsg);
            }
            if (now > timeout) {
              waiting = false;
              connectStatus.setText("Timeout on " + selectPort);
              thePort.stop();
              have_port = false;
            }
          }
        }
      } else {
        println("Error - SerialOSC.connect() method requires .doMenu()!");
      }
    } else {
      // Disconnect Serial Port
      have_port = false;
      thePort.stop();
      connectStatus.setText("Disconnected");
      connectButton.setText("Connect");
      menu.setEnabled(true);
    }
  }

  // ******************************************************
  // readString()
  // Parses the Serial Port buffer and places SerialOSCmsg in msgStack
  public void readString() {
    //println("Got a Message");
    char inChar;
    int parseMode = 0;
    int num_data = 0;
    int dataCtr = 0;
    int dataPtr = 0;
    String tempTypes = "";
    String dataString = "";
    boolean done = false;
    SerialOSCmsg msg = new SerialOSCmsg();
    int flushCtr = 0;
    byte[] byteArray = new byte[4];
    // ************************************
    // The main loop reads the chars out of the Serial buffer
    while (thePort.available () > 0) {
      inChar = thePort.readChar();
      //print(inChar);
      //println(", 0x" + hex(inChar, 2));
      
      // How we interpret the character depends on the parse mode
      switch (parseMode) {
        // ****************************
        // Mode 0: Looking for Start Char '/'
        case 0: 
          {
            if (inChar == start_char) {
              parseMode = 1;
              num_data = 0;
              dataCtr = 0;
              dataPtr = 0;
              msg.method = "";
              msg.path = "";
              tempTypes = "";
            }
            break;
          }
        // ****************************
        // Mode 1: Process path/method
        case 1: 
          {
            if (inChar == 0) {  // Null terminated - end of OSC String
              parseMode = 2;
            } 
            else if (inChar == start_char) {  // Path delimiter - add "method" to path
              msg.path += '/' + msg.method;
              msg.method = "";
            } 
            else if ((inChar > 0x1F) && (inChar < 0x7F)) {  // Text char - add to current method
              msg.method += inChar;
            } 
            else {
              parseMode = 0;  // ? Error - Back to square one
            }
            break;
          }
        // ****************************
        // Mode 2 - Get to start of OSC Type String
        case 2: 
          {
            if (inChar == ',') {  // comma - start of the type tag string
              parseMode = 3;
              flushCtr = 1;
            } 
            else if (inChar == start_char) {  // Seem to have a 2nd message appended
              msgStack.add(msg);              // So, add simple command msg to stack
              parseMode = 1;                  // and clear
              num_data = 0;
              dataCtr = 0;
              dataPtr = 0;
              msg.method = "";
              msg.path = "";
              tempTypes = "";
            } 
            else if (inChar != 0) {           // Just skip bytes if null padding to 4-byte
              parseMode = 0;                  // Or, if not null, ',', or '/' then error
            }
            break;
          }
        // ****************************
        // Mode 3: Process OSC Type String
        case 3: 
          {
            flushCtr = (flushCtr + 1) % 4;    // Everything on 4-byte boundaries
            if (inChar == 0) {                // Null Terminator
              if (dataCtr == 0) {             // If no data, then we're done
                done = true;
                parseMode = 5;
              } 
              else if (flushCtr == 0) {      // Or if on a 4-byte boundary, then done
                parseMode = 4;
                num_data = dataCtr;
                dataCtr = 0;
                dataPtr = 0;
                dataString = "";
              }
            }
            else if (inChar == start_char) {   // Some sort of error
              parseMode = 1;
              num_data = 0;
              dataCtr = 0;
              dataPtr = 0;
              msg.method = "";
              msg.path = "";
              tempTypes = "";
            } 
            // Error if not a recognized data type
            else if ((inChar != 'b') && (inChar != 'f') && (inChar != 'i') && (inChar != 's') ) {
              parseMode = 0;
            } 
            else {                              // Add the data type char to the type stack
              tempTypes += inChar;
              dataCtr++;
            }
            break;
          }
        // ****************************
        // Mode 4: Assemble Bytes into data
        case 4: 
          {
            flushCtr = (flushCtr + 1) % 4;
            char test_char = tempTypes.charAt(dataCtr);
            if (test_char == 's') {
              if (inChar == 0) {
                if (flushCtr == 0) { 
                  dataCtr++;
                  dataPtr = 0;
                  msg.add(dataString);
                  dataString = "";
                }
              } 
              else {
                dataString += inChar;
              }
            } 
            else if (test_char == 'i') {
              byteArray[dataPtr++] = byte(inChar);
              if (flushCtr == 0) {
                dataCtr++;
                dataPtr = 0;
                msg.add(bytes2int(byteArray));
              }
            } 
            else if (test_char == 'f') {
              byteArray[dataPtr++] = byte(inChar);
              if (flushCtr == 0) {
                dataCtr++;
                dataPtr = 0;
                msg.add(bytes2float(byteArray));
              }
            }
            if (dataCtr >= num_data) {
              done = true;
              parseMode = 5;
            }
            break;
          }
        // ****************************
        // Mode 5: Just here to flush out the rest of the buffer
        case 5: 
          {
            break;
          }
      }
    }

    if (done || (parseMode == 2)) {  // If we finished a message or just had cmd with no ","
      msgStack.add(msg);
    } 
    return;
  }
  
  // ******************************************************
  // send(SerialOSCmsg)
  // Sends SerialOSCmsg
  public void send(SerialOSCmsg msg) {
    if (have_port) {
      int pad = 0;
      byte nil = 0;
      
      thePort.write(msg.path + '/' + msg.method);
      thePort.write(nil);
      pad = (msg.path.length() + msg.method.length() + 2) % 4;
      if (pad > 0) {
        for (int ctr = 0; ctr < (4 - pad); ctr++) {
          thePort.write(nil);
        }
      }
      
      thePort.write(',');
      if (msg.types.length() > 0) thePort.write(msg.types);
      thePort.write(nil);
      pad = (msg.types.length() + 2) % 4;
      if (pad > 0) {
        for (int ctr = 0; ctr < (4 - pad); ctr++) {
          thePort.write(nil);
        }
      }
      
      if (msg.types.length() > 0) {
        for (int ctr = 0; ctr < msg.types.length();ctr++) {
          switch (msg.types.charAt(ctr)) {
            case 's': {
              String s = msg.getString(ctr);
              thePort.write(s);
              thePort.write(nil);
              pad = (s.length() + 1) % 4;
              if (pad > 0) {
                for (int p_ctr = 0; p_ctr < (4 - pad); p_ctr++) {
                  thePort.write(nil);
                }
              }
              break;
            }
            case 'i': {
              int i = msg.getInt(ctr);
              byte[] b = int2bytes(i);
              for (int b_ctr = 0; b_ctr < 4; b_ctr++) {
                thePort.write(b[b_ctr]);
              }
              break;
            }
            case 'f': {
              float f = msg.getFloat(ctr);
              byte[] b = float2bytes(f);
              for (int b_ctr = 0; b_ctr < 4; b_ctr++) {
                thePort.write(b[b_ctr]);
              }
              break;
            }
          }
        }
      }
    }
  }
  
  // ******************************************************
  // available() returns true if there is a message on the stack
  public boolean available() {
    boolean avl = false;
    if (msgStack.size() > 0) avl = true;
    return avl;
  }
  
  // ******************************************************
  // read() removes the top SerailOSCmsg from the msgStack and returns it
  public SerialOSCmsg read() {
    SerialOSCmsg msg = new SerialOSCmsg();
    if (msgStack.size() > 0) {
      msg = (SerialOSCmsg)msgStack.get(0);
      msgStack.remove(0);
    }
    return msg;
  }
  
  // ******************************************************
  // The following private functions perform the float/int
  // to/from a 4 byte array and are used to format both
  // incoming and outgoing  OSC messages
  
  private byte[] float2bytes(float f) {
    int raw=Float.floatToRawIntBits(f);
    byte[] buf=int2bytes(raw);
    return buf;
  }

  private byte[] int2bytes(int i) {
    byte[] buf=new byte[4];
    buf[0]=(byte)(i>>>24);
    buf[1]=(byte)(i>>16 & 0xff);
    buf[2]=(byte)(i>>8 & 0xff);
    buf[3]=(byte)(i & 0xff);
    return buf;
  }

  private float bytes2float(byte[] b) {
    int i = bytes2int(b);
    return Float.intBitsToFloat(i);
  }

  private int bytes2int(byte[] b) {
    int i = (int(b[0]) << 24) + (int(b[1]) << 16) + (int(b[2]) << 8) + int(b[3]);
    return i;
  }
}

// ******************************************************************************
class SerialOSCmsg {
  // The OSCmsg class object holds the data contained in an incoming or outgoing OSC message

  String path;      // the path string - everything up to the final "/" in the OSC Address Pattern
  String method;    // the method string - the characters following the final "/"
  String types;     // the OSC types - 'i', 'f', or 's'
  ArrayList data;   // the OSC data - an ArrayList of OSCvalue objects
  
  // *********************************
  // Constructor
  SerialOSCmsg() {
    data = new ArrayList();
    method = "";
    path = "";
    types = "";
  }
  
  // *********************************
  // Constructor - called with String sets Message method to String
  SerialOSCmsg(String m) {
    data = new ArrayList();
    method = m;
    path = "";
    types = "";
  }
  
  // *********************************
  // add() methods - adds an integer, float, or string data value to the message
  public void add(int i) {
    types += 'i';
    data.add(new OSCvalue(i));
  }

  public void add(float f) {
    types += 'f';
    data.add(new OSCvalue(f));
  }

  public void add(String s) {
    types += 's';
    data.add(new OSCvalue(s));
  }
  
  // *********************************
  // get methods return OSC data at index as the requested data type
  public int getInt(int i) {
    OSCvalue temp = (OSCvalue)data.get(i);
    return temp.getInt();
  }

  public float getFloat(int i) {
    OSCvalue temp = (OSCvalue)data.get(i);
    return temp.getFloat();
  }

  public String getString(int i) {
    OSCvalue temp = (OSCvalue)data.get(i);
    return temp.getString();
  }
  
  // *********************************
  // toString returns the complete OSC message as a String
  // This is also used by print() and println() to allow the statement:
  // println(oscMessage);
  public String toString() {
    String s = "OSC: ";
    if (path != null) s += path;
    s += '/' + method + ": ";
    if (types.length() > 0) {
      s += types;
      s += " : ";
      for (int ctr = 0; ctr < types.length(); ctr++) {
        //println("Length = " + str(types.length()) + ", " + str(ctr) + ", Types = " + types);
        s += getString(ctr);
        if (ctr < (types.length() - 1))s += ", ";
      }
    }
    return s;
  }
  
  
  // *********************************
  //  The nested OSCvalue class provides an object to stick in the data ArrayList 
  private class OSCvalue {

    int intValue;
    float floatValue;
    String strValue;
    char type;
  
    OSCvalue(int i) {
      intValue = i;
      type = 'i';
    }
  
    OSCvalue(float f) {
      floatValue = f;
      type = 'f';
    }
  
    OSCvalue(String s) {
      strValue = s;
      type = 's';
    }
  
    float getFloat() {
      float f = 0.;
      switch (type) {
      case 'i': 
        {
          f = float(intValue);
          break;
        }
      case 'f': 
        {
          f = floatValue;
          break;
        }
      case 's': 
        {
          f = float(strValue);
          break;
        }
      }
      return f;
    }
  
    int getInt() {
      int i = 0;
      switch (type) {
      case 'i': 
        {
          i = intValue;
          break;
        }
      case 'f': 
        {
          i = int(floatValue);
          break;
        }
      case 's': 
        {
          i = int(strValue);
          break;
        }
      }
      return i;
    }
  
    String getString() {
      String s = "";
      switch (type) {
      case 'i': 
        {
          s = str(intValue);
          break;
        }
      case 'f': 
        {
          s = str(floatValue);
          break;
        }
      case 's': 
        {
          s = strValue;
          break;
        }
      }
      return s;
    }
  }
  
}




